﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using PhoneNetTools.Dns.Records;

namespace PhoneNetTools.Dns
{
    /// <summary>
    /// A wrapper that uses the generic <see cref="DnsResolver"/> internally
    /// to provide convenient access to certain kinds of DNS requests to the user.
    /// </summary>
    public class DnsHelper : IDisposable
    {
        private DnsResolver _dnsResolver;

        /// <summary>
        /// Begins getting a host entry (IPv4 entry) for the given host name.
        /// </summary>
        /// <param name="hostName">Name of the host to perform a query for.</param>
        /// <param name="callback">The callback to invoke when the operation has finished.</param>
        /// <param name="state">A user state to identify the operation.</param>
        /// <returns>An async result.</returns>
        public IAsyncResult BeginGetHostEntry(string hostName, AsyncCallback callback, object state)
        {
            if (string.IsNullOrEmpty(hostName))
            {
                throw new ArgumentException("No valid host name given.");
            }

            CleanUp();

            // create message
            var message = new Message();
            message.IsRecursionDesired = true;

            // create question
            var question = new Question(QType.A);
            question.Name = hostName;
            message.Questions.Add(question);

            // let the resolver do the work
            _dnsResolver = new DnsResolver();
            var ar = _dnsResolver.BeginDnsRequest(message, callback, state);
            return ar;
        }

        /// <summary>
        /// Ends an ongoing get host entry operation.
        /// Throws any pending exceptions.
        /// </summary>
        /// <param name="asyncResult">The async result returned by the <see cref="BeginGetHostEntry"/> method.</param>
        /// <returns>The result of obtained IP addresses.</returns>
        public IEnumerable<IPAddress> EndGetHostEntry(IAsyncResult asyncResult)
        {
            var ar = asyncResult as AsyncResult<Message>;
            if (ar == null)
            {
                throw new ArgumentException("Invalid IAsyncResult used to called EndGetHostEntry.");
            }

            // use the async result to end the operation
            var message = _dnsResolver.EndDnsRequest(asyncResult);

            // check to see if the message is a valid result
            if (message.ResponseCode != ResponseCode.NoError)
            {
                var errorCode = Enum.GetName(typeof(ResponseCode), message.ResponseCode);
                var errorMessage = string.Format(CultureInfo.InvariantCulture, "The server returned an error: {0} (code {1}", errorCode, message.ResponseCode);
                throw new DnsException(errorMessage, message.ResponseCode);
            }

            // create the result
            var result = message.AnswerRecords
                .OfType<ResourceRecordA>()
                .Select(answer => answer.IPAddress)
                .ToList();

            return result;
        }

        private void CleanUp()
        {
            if (_dnsResolver != null)
            {
                _dnsResolver.Dispose();
                _dnsResolver = null;
            }
        }

        #region IDisposable

        private bool _isDisposed;

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                if (disposing)
                {
                    CleanUp();
                }

                // Free your own state (unmanaged objects).
                // Set large fields to null.
                _isDisposed = true;
            }
        }

        #endregion
    }
}
